#include <chrono>
#include <random>
#include <fstream>
#include <utility>

#include "macros/errors.h"
#include "backend/program/program_mainc_group.h"
// #include "backend/kernel_interface/kernel_scheduler.h"
#include "backend/program/program_impl_mainc_group.h"

namespace txdnn {
namespace backend {

fs::path CreateUniquePath(std::string prefix="txdnn_") {
  auto now = std::chrono::system_clock::now();
  auto duration = now.time_since_epoch();
  auto mils = std::chrono::duration_cast<std::chrono::milliseconds>(duration).count();

  std::random_device rd;
  std::mt19937 gen(rd());
  std::uniform_int_distribution<> dis(0, 9999);
  auto rand = dis(gen);

  fs::path tmp_dir = fs::temp_directory_path();
  auto tmp_final = tmp_dir / (prefix + std::to_string(mils) + "_" + std::to_string(rand));

  return tmp_final;
}

/* class TmpDir Implementation */
TmpDir::TmpDir(TXDNN_STR_VIEW prefix) : path_{fs::temp_directory_path()} {
  std::string p{prefix.empty() ? "" : (prefix[0] == '_' ? "" : "_")};
  path_ /= CreateUniquePath();
  fs::create_directory(path_);
}

int TmpDir::Execute(TXDNN_STR_VIEW cmd, TXDNN_STR_VIEW args) const {
  // TODO: Implement
  return 0;
}

TmpDir::~TmpDir() {
  // TODO: Implement
}

/* class ProgramImplMainCGroup Utils */

// static TxdaModulePtr CreateModule(const fs::path& bin_file) {
//   txdaModule_t raw_m;
//   auto status = txdaModuleLoad(&raw_m, bin_file.string().c_str());
//   TxdaModulePtr m{raw_m};
//   if (status != TXDA_SUCCESS) {
//     TXDNN_THROW("Failed to create module from file: " + bin_file);
//   }
//   return m;
// }

// template <typename T>
// TxdaModulePtr CreateModuleInMem(const T& blob) {
//   txdaModule_t raw_m;
//   auto status = txdaModuleLoadData(&raw_m, reinterpret_cast<const void*>(blob.data()));
//   TxdaModulePtr m{raw_m};
//   if (status != TXDA_SUCCESS) {
//     TXDNN_THROW("Failed to create module from memory");
//   }
//   return m;
// }

/* main.c build utils: WriteFile() */
inline void WriteFile(TXDNN_STR_VIEW content, const fs::path& p) {
  std::ofstream f{p};
  if (f.write(content.data(), content.size()).fail()) {
    TXDNN_THROW("Failed to write file: " + p);
  }
}

inline void WriteFile(const std::vector<char>& content, const fs::path& p) {
  std::ofstream f{p, std::ios::binary};
  if (f.write(content.data(), content.size()).fail()) {
    TXDNN_THROW("Failed to write file: " + p);
  }
}

inline void WriteFile(const std::vector<uint8_t>& content, const fs::path& p) {
  std::ofstream f{p, std::ios::binary};
  if (f.write(reinterpret_cast<const char*>(content.data()), content.size()).fail()) {
    TXDNN_THROW("Failed to write file: " + p);
  }
}

/* main.c build utils: WriteFile() and */
static fs::path BuildImplMainC(const TmpDir& tmp_dir,
                               const fs::path& file_name,
                               std::string kernel_src,
                               std::string build_params,
                               const TargetProperties& target,
                               const bool test_mode) {
  if (!test_mode) {
    auto inc_list = GetKernelIncList();
    fs::create_directories(tmp_dir);
    for (const auto& inc : inc_list) {
      auto inc_code = GetKernelInc(inc);
      WriteFile(inc_code, tmp_dir / inc);
    }
  }

  kernel_src += "\nint main() {}\n";
  WriteFile(kernel_src, tmp_dir / file_name);

  // TODO add build_params

  // build object
  auto bin_file = make_object_file_name(tmp_dir / file_name);

  // compile
  // TODO

  return bin_file;
}

fs::path BuildMainC(const TmpDir& tmp_dir,
                    const fs::path& file_name,
                    TXDNN_STR_VIEW kernel_src,
                    std::string build_params,
                    const TargetProperties& target) {
  // TODO special build_params to be added
  return BuildImplMainC(tmp_dir, file_name, std::string{kernel_src}, build_params, target, false);
}


/* ProgramImplMainCGroup Member Function */
void ProgramImplMainCGroup::BuildCodeObjectFromFile(std::string& params, 
                                                    TXDNN_STR_VIEW src,
                                                    const fs::path& p) {
  dir_.emplace(p.filename().string());
  bin_file_ = make_object_file_name(dir_.value() / p);

  if (p.extension() == ".c") {
    // build .bin file
    bin_file_ = BuildMainC(dir_.value(), p, src, params, target_);
  }

  // add build params
  // TODO

  // build code object
  // TODO
}

void ProgramImplMainCGroup::BuildCodeObjectFromMemory(const std::string& params,
                                                      TXDNN_STR_VIEW src,
                                                      const fs::path& p) {
  // TODO
  // different sources call different compilation tools 
}


void ProgramImplMainCGroup::BuildCodeObject(std::string params, const std::string& kernel_src) {
  const auto src = [&]() -> TXDNN_STR_VIEW {
    if (program_path_.extension() == ".mlir") {
      return {}; // TODO
    }
    if (!kernel_src.empty()) {
      return kernel_src;
    }
    return GetKernelSrc(program_path_);
  }();

  // TODO add compile options
  if (program_path_.extension() == ".c") {
    params += " -O2";
  }

  // build code object
  BuildCodeObjectFromFile(params, src, program_path_);
}

ProgramImplMainCGroup::ProgramImplMainCGroup(const fs::path& program_path, const fs::path& file_bin)
  : program_path_(program_path), bin_file_(file_bin) {
  // module_ = CreateModule(bin_file_);
}

ProgramImplMainCGroup::ProgramImplMainCGroup(const fs::path& program_path, const std::vector<char>& blob)
  : program_path_(program_path) {
  // TODO: set EnvValue for different architecture
  // module_ = CreateModuleInMem(blob);
}

ProgramImplMainCGroup::ProgramImplMainCGroup(const fs::path& program_path, const std::vector<uint8_t>& blob)
  : program_path_(program_path) {
  // TODO: set EnvValue for different architecture
  // module_ = CreateModuleInMem(blob);
}

ProgramImplMainCGroup::ProgramImplMainCGroup(const fs::path& program_path, 
                                             std::string params,
                                             const TargetProperties& target,
                                             const std::string& kernel_src)
  : program_path_(program_path), target_(target) { }


} // namespace backend
} // namespace txdnn